"use client"; import { useEffect, useState, useCallback } from "react"; import { useParams, useRouter } from "next/navigation"; import { useI18n } from "@/lib/i18n/provider"; import { ResponsiveLayout } from "@/components/layout"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { Badge } from "@/components/ui/badge"; import { toast } from "@/hooks/useToast"; import { fetchDocument, type Document, } from "@/features/agent/services/documentApi"; import { fetchChunks, executeChunking, updateChunk, deleteChunks, type DocumentChunk, } from "@/features/agent/services/chunkApi"; import { ChevronLeft, ChevronRight, Scissors, Pencil, Trash2, Loader2, FileText, CheckCircle2, Clock, XCircle, } from "lucide-react"; type ChunkMethod = "char_count" | "separator"; interface DocumentDetailPageProps { embedded?: boolean; docId?: number; onBack?: () => void; } export default function DocumentDetailPage(props: DocumentDetailPageProps = {}) { const { embedded = false, docId: propDocId, onBack } = props; const params = useParams<{ docId?: string }>(); const router = useRouter(); useI18n(); const docIdNum = propDocId ?? Number(params.docId ?? 0); const [doc, setDoc] = useState(null); const [loadingDoc, setLoadingDoc] = useState(true); const [docError, setDocError] = useState(""); const [chunks, setChunks] = useState([]); const [loadingChunks, setLoadingChunks] = useState(false); const [chunkPage, setChunkPage] = useState(1); const [chunkTotalPage, setChunkTotalPage] = useState(1); const [chunkTotal, setChunkTotal] = useState(0); const pageSize = 10; const [method, setMethod] = useState("char_count"); const [chunkSize, setChunkSize] = useState(500); const [separator, setSeparator] = useState(""); const [executing, setExecuting] = useState(false); const [editChunk, setEditChunk] = useState(null); const [editContent, setEditContent] = useState(""); const [saving, setSaving] = useState(false); const [showConfirm, setShowConfirm] = useState(false); const loadDoc = useCallback(async () => { if (!docIdNum || isNaN(docIdNum)) { setDocError("文档 ID 无效"); setLoadingDoc(false); return; } try { setLoadingDoc(true); const d = await fetchDocument(docIdNum); setDoc(d); setDocError(""); } catch (e: unknown) { const msg = e instanceof Error ? e.message : "加载文档失败"; setDocError(msg); } finally { setLoadingDoc(false); } }, [docIdNum]); const loadChunks = useCallback(async (page: number = 1) => { if (!docIdNum || isNaN(docIdNum)) return; try { setLoadingChunks(true); const res = await fetchChunks(docIdNum, page, pageSize); setChunks(res.chunks || []); setChunkTotalPage(res.total_page || 1); setChunkTotal(res.total || 0); } catch { setChunks([]); } finally { setLoadingChunks(false); } }, [docIdNum, pageSize]); useEffect(() => { loadDoc(); loadChunks(chunkPage); }, [loadDoc, loadChunks, chunkPage]); useEffect(() => { const hasPending = chunks.some( (c) => c.embedding_status === "pending" || c.embedding_status === "processing" ); if (!hasPending) return; const timer = setInterval(() => loadChunks(chunkPage), 2500); return () => clearInterval(timer); }, [chunks, loadChunks, chunkPage]); const handleExecute = async () => { if (method === "separator" && !separator) { toast.error("请输入分隔符"); return; } try { setExecuting(true); const res = await executeChunking(docIdNum, { method, chunk_size: method === "char_count" ? chunkSize : undefined, separator: method === "separator" ? separator : undefined, }); setChunks(res.chunks || []); setChunkPage(1); toast.success(`分段完成,共 ${res.chunk_count} 段`); } catch (e: unknown) { const msg = e instanceof Error ? e.message : "分段失败"; toast.error(msg); } finally { setExecuting(false); } }; const handleDelete = async () => { try { await deleteChunks(docIdNum); setChunks([]); setChunkPage(1); setChunkTotalPage(1); setChunkTotal(0); setShowConfirm(false); toast.success("分段已删除"); } catch (e: unknown) { const msg = e instanceof Error ? e.message : "删除失败"; toast.error(msg); } }; const openEdit = (chunk: DocumentChunk) => { setEditChunk(chunk); setEditContent(chunk.content); }; const handleSaveEdit = async () => { if (!editChunk || !editContent.trim()) return; try { setSaving(true); await updateChunk(docIdNum, editChunk.id, editContent); toast.success("分段已更新,正在重新向量化"); setEditChunk(null); loadChunks(); } catch (e: unknown) { const msg = e instanceof Error ? e.message : "保存失败"; toast.error(msg); } finally { setSaving(false); } }; const statusBadge = (status: string) => { switch (status) { case "completed": return ( 已向量化 ); case "processing": return ( 向量化中 ); case "pending": return ( 待向量化 ); case "failed": return ( 向量化失败 ); default: return {status}; } }; const truncate = (text: string, maxLen: number) => { if (text.length <= maxLen) return text; return text.slice(0, maxLen) + "..."; }; const headerContent = (

{loadingDoc ? "加载中..." : doc?.title || "文档详情"}

{doc && (
{doc.type} {doc.status === "published" ? "已发布" : "草稿"} · {chunkTotal > 0 ? `${chunkTotal} 个分段` : "未分段"}
)}
); const chunkControls = (

分段操作

{method === "char_count" ? (
setChunkSize(Number(e.target.value) || 500)} />
) : (
setSeparator(e.target.value)} />
)} {chunks.length > 0 && ( )}
{chunks.length > 0 && (

执行新分段将自动替换旧分段并重新向量化

)}
); const chunkList = (

分段列表 {loadingChunks && }

{chunks.length === 0 && !loadingChunks ? (

尚未分段

选择分段方式并执行,系统将自动为每段生成向量索引

) : ( <> {chunks.map((chunk) => (
第 {chunk.chunk_index + 1} 段 {statusBadge(chunk.embedding_status)} {chunk.content.length} 字

{truncate(chunk.content, 200)}

))} {chunkTotalPage > 1 && (
第 {chunkPage}/{chunkTotalPage} 页,共 {chunkTotal} 条
)} )}
); const mainContent = (
{docError ? (

{docError}

) : loadingDoc ? (
) : doc ? ( <> {chunkControls} {chunkList} ) : null}
); const editDialog = ( { if (!open) setEditChunk(null); }} > 编辑第 {(editChunk?.chunk_index ?? 0) + 1} 段 修改后会自动重新向量化该分段